package xuan.lib;

import java.io.IOException;
import java.util.LinkedList;

import java.util.List;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * main class :
 * reconstruct interactively target tree from source and edit log,
 * each operation can be previewed, confirmed and inversed.
 * 
 * @author xuan
 *
 */
public class Merge {
	private Document source;
	private Document merged;
	private EditLog eLog;
	private List<Operation> undoList= new LinkedList<Operation>();
	
	public Merge(String[] args) throws XPathExpressionException, ParserConfigurationException, SAXException, IOException{
		source = DocUtils.getNormalizedDocument(args[0]);
		merged = DocUtils.getNormalizedDocument(args[1]);
		eLog = new EditLog(args[2], source, merged);
	}
	
	
//	/**
//	 * for replaying all operations
//	 */
//	public void merge(){
//		//may be need a timer to segment each operation 
//		while(eLog.size() !=0){
//			previewFirstOperation();
//			executeFirstOperation();
//		}
//	}
	
//	public void previewFirstOperation() {
//		Operation op = eLog.getOperation(0);
//		
//		switch(op.getType()){
//		case UPDATE:
//			previewUpdate(op);
//			break;
//		case INSERT:
//			previewInsert(op);
//			break;
//		case MOVE:
//			previewMove(op);
//			break;
//		case COPY:
//			previewCopy(op);
//			break;
//		default:
//			previewDelete(op);
//		}
//	}
//
//	public void executeFirstOperation() {
//		Operation op = eLog.getOperation(0);
//		
//		switch(op.getType()){
//		case UPDATE:
//			update(op);
//			break;
//		case INSERT:
//			insert(op);
//			break;
//		case MOVE:
//			move(op);
//			break;
//		case COPY:
//			copy(op);
//			break;
//		default:
//			delete(op);
//		}
//		
//		eLog.remove(op);
//	}
	

	/**
	 * preview an operation
	 * @param op operation to preview
	 */
	public void preview(Operation op) {
		switch(op.getType()){
		case UPDATE:
			previewUpdate(op);
			break;
		case INSERT:
			previewInsert(op);
			break;
		case MOVE:
			previewMove(op);
			break;
		case DELETE:
			previewDelete(op);
			break;
//		default://COPY
//			previewCopy(op);
    	}
		
		op.setPreviewed(true);
	}
	
	private void previewUpdate(Operation op) {
		Node sourceNode = op.getSourceNode();
		Node mergedNode = op.getMergedNode();
		
		// there are nearly never update from one node to another which type is different
		// in case TEXT_NODE don't need update directly in the tree in preview mode
		
		if(sourceNode.getNodeType()==Node.ELEMENT_NODE && mergedNode.getNodeType()==Node.ELEMENT_NODE){
			//create a new node
			Node updateNode = mergedNode.cloneNode(false);
			source.adoptNode(updateNode);
			
			// Move all children
			while (sourceNode.hasChildNodes()) {
				updateNode.appendChild(sourceNode.getFirstChild());
			}
			
			// Replace the old node with the new node			
			sourceNode.getParentNode().replaceChild(updateNode, sourceNode);
			
			//save temporarily updated node
			op.setTempNode(updateNode);
		}
	}
	
	private void previewInsert(Operation op) {
		Node mergedNode = op.getMergedNode();
		Node parentNode = op.getTargetNode();// new parent of the node
		Node siblingNode = (parentNode.hasChildNodes())? parentNode.getChildNodes().item(op.getPos()):null;// new right sibling of the node
		Node insertNode = mergedNode.cloneNode(false);//get new node from target
		
		parentNode.insertBefore(source.adoptNode(insertNode), siblingNode);
		
		//save new inserted node as source node of operation
		op.setSourceNode(insertNode);
	}

	private void previewDelete(Operation op) {
		//nothing to do in preview mode
	}

	private void previewMove(Operation op) {
		Node sourceNode = op.getSourceNode();
		Node parentNode = op.getTargetNode();//new parent
		Node siblingNode = (parentNode.hasChildNodes())? parentNode.getChildNodes().item(op.getPos()):null;//new right sibling
		Node insertNode = sourceNode.cloneNode(false);//copy source node
		
		//insert copied node at new place - don't need to delete source node in preview mode
		parentNode.insertBefore(source.adoptNode(insertNode), siblingNode);
		
		//also copy all children of source node to copied node
		if (sourceNode.getNodeType()==Node.ELEMENT_NODE){	
			NodeList childNodes = sourceNode.getChildNodes();
			
			for(int i=0; i<childNodes.getLength();i++){
				insertNode.appendChild(source.adoptNode(childNodes.item(i).cloneNode(true)));
			}
			
		}
		
		//save temporarily copied node
		op.setTempNode(insertNode);
	}

//	private void previewCopy(Operation op) {
//		Node sourceNode = op.getSourceNode();
//		Node parentNode = op.getTargetNode();//new parent of copied node
//		Node siblingNode = (parentNode.hasChildNodes())? parentNode.getChildNodes().item(op.getPos()):null;//new right sibling of copied node
//		Node copiedNode = sourceNode.cloneNode(true);//copied node
//		
//		//insert copied node at new place
//		parentNode.insertBefore(source.adoptNode(copiedNode), siblingNode);
//		//save temporarily copied node
//		op.setTempNode(copiedNode);
//	}

	/**
	 * apply an operation to the tree
	 * @param op operation to apply
	 */
	public void execute(Operation op) {
		switch(op.getType()){
		case UPDATE:
			update(op);
			break;
		case INSERT:
			insert(op);
			break;
		case MOVE:
			move(op);
			break;
		case DELETE:
			delete(op);
			break;
//		default://COPY
//			copy(op);
    	}
		
		op.setExecuted(true);
		undoList.add(op);
	}
	
	private void update(Operation op) {
		Node sourceNode = op.getSourceNode();
		Node mergedNode = op.getMergedNode();
		Node updateNode;
		
		if(!op.isPreviewed()){			
			if(sourceNode.getNodeType()==Node.ELEMENT_NODE && mergedNode.getNodeType()==Node.ELEMENT_NODE){
				//copy merged node and move all children to
				updateNode = mergedNode.cloneNode(false);
	
				// Move all the children
				while (sourceNode.hasChildNodes()) {
					updateNode.appendChild(sourceNode.getFirstChild());
				}
				
				// Replace the source node with the updated node
				sourceNode.getParentNode().replaceChild(updateNode, sourceNode);
				
				// Coz change source node by updated node, must update operations which subject (source, target) was source node
				for(int i=1;i<eLog.size();i++){
					Operation op2 = eLog.getOperation(i);
					if(op2.getSourceNode()!=null && op2.getSourceNode().equals((Node)sourceNode)){
						op2.setSourceNode(updateNode);
					}
					
					if(op2.getTargetNode()!=null && op2.getTargetNode().equals((Node)sourceNode)){
						op2.setTargetNode(updateNode);
					}
				}
				
				//save updated node as source node and source node as archived node of operation
				op.setSourceNode(updateNode);
				op.setTempNode(sourceNode);
			}
			else{//TEXT_NODE, just copy the content of node in target tree
				//archive old value
				Node tempNode = sourceNode.cloneNode(true);
				op.setTempNode(tempNode);
				//take new value
				sourceNode.setNodeValue(mergedNode.getNodeValue());				
			}
		}
		else{//already previewed
			//if node is TEXT_NODE, must update content coz nothing done in preview mode
			if(sourceNode.getNodeType()==Node.TEXT_NODE && mergedNode.getNodeType()==Node.TEXT_NODE){
				//archive old value
				Node tempNode = sourceNode.cloneNode(true);
				op.setTempNode(tempNode);
				//take new value
				sourceNode.setNodeValue(mergedNode.getNodeValue());
			}
			else{
				// Coz change source node by updated node, must update operations which subject (source, target) was source node
				updateNode = op.getTempNode();
				for(int i=0;i<eLog.size();i++){
					Operation op2 = eLog.getOperation(i);
					
					if(op2.getSourceNode()!=null && op2.getSourceNode().equals((Node)sourceNode)){
						op2.setSourceNode(updateNode);
					}
					
					if(op2.getTargetNode()!=null && op2.getTargetNode().equals((Node)sourceNode)){
						op2.setTargetNode(updateNode);
					}
				}
				
				//save updated node as source node and source node as archived node of operation
				op.setSourceNode(updateNode);
				op.setTempNode(sourceNode);
			}
		}
	}
	
	private void insert(Operation op) {
		Node insertNode;
		
		if(!op.isPreviewed()){
			Node mergedNode = op.getMergedNode();
			Node parentNode = op.getTargetNode();
			Node siblingNode = (parentNode.hasChildNodes())? parentNode.getChildNodes().item(op.getPos()):null;
			insertNode = mergedNode.cloneNode(false);
			
			parentNode.insertBefore(source.adoptNode(insertNode), siblingNode);
		}
		else{
			insertNode = op.getSourceNode();
		}
		
		//once node is added, update operations that depend on this adding
		if(op.getDeps(false)!=null){
			List<Operation> dOps = op.getDeps(false);  
			
			for(int i=0;i< dOps.size();i++){
				Operation op2 = (Operation)dOps.get(i);
				op2.setTargetNode(insertNode);
				//enable operation if all previous operations were executed
				
				if(op2.getNbPrevs()>0) {
					op2.setNbPrevs(op2.getNbPrevs()-1);
				}
				if(op2.getNbPrevs()==0){
					op2.setEnabled(true);
				}
			}
		}
	}
	
	private void delete(Operation op) {
		Node deleteNode = op.getSourceNode();
		
		//archive deleted subtree
		Node tempNode = deleteNode.cloneNode(true);
		op.setTempNode(tempNode);
		op.setTargetNode(deleteNode.getParentNode());
		op.setPos(op.getSourceNodePath().getNodeIndex());
		
		//delete really subtree
		deleteNode.getParentNode().removeChild(deleteNode);
		op.setSourceNode(null);
	}
	
	private void move(Operation op) {
		Node sourceNode = op.getSourceNode();		
		Node insertNode;
		
		if(!op.isPreviewed()){//insert node at new place
			Node mergedNode = op.getMergedNode();
			Node parentNode = op.getTargetNode();
			Node siblingNode = (parentNode.hasChildNodes())? parentNode.getChildNodes().item(op.getPos()):null;
			insertNode = mergedNode.cloneNode(false);
			
			parentNode.insertBefore(source.adoptNode(insertNode), siblingNode);
		}
		else{
			insertNode = op.getTempNode();
		}
		
		//archive parent of source node before move it 
		op.setTempNode(sourceNode.getParentNode());
		op.setPos(op.getSourceNodePath().getNodeIndex());
		
		//replace temporary node by source node
		insertNode.getParentNode().replaceChild(sourceNode, insertNode);
		
		//once node is moved, update operations that depend on this changing
		if(op.getDeps(false)!=null){
			List<Operation> dOps = op.getDeps(false);  
			
			for(int i=0;i< dOps.size();i++){
				Operation op2 = (Operation)dOps.get(i);
				if(op2.getNbPrevs()>0) {
					op2.setNbPrevs(op2.getNbPrevs()-1);
				}
				if(op2.getNbPrevs()==0){
					op2.setEnabled(true);
				}
			}
		}
	}
	
//	private void copy(Operation op) {
//		if(!op.isPreviewed()){
//			Node sourceNode = op.getSourceNode();
//			Node parentNode = op.getTargetNode();
//			Node siblingNode = (parentNode.hasChildNodes())? parentNode.getChildNodes().item(op.getPos()):null;
//			Node copiedNode = sourceNode.cloneNode(true);
//			parentNode.insertBefore(source.adoptNode(copiedNode), siblingNode);
//			op.setTempNode(copiedNode);
//		}
//	}

	
	/**
	 * undo an previewed operation
	 * @param op previewed operation
	 */
	public void undoAPreview(Operation op) {
		if(op.isPreviewed()){
			switch(op.getType()){
			case UPDATE:
				undoPreUpdate(op);
				break;
			case INSERT:
				undoPreInsert(op);
				break;
			case MOVE:
				undoPreMove(op);
				break;
			case DELETE:
				undoPreDelete(op);
				break;
//			default://COPY
//				undoPreCopy(op);
	    	}
			
			op.setPreviewed(false);
		}
	}
	
	private void undoPreUpdate(Operation op) {
		Node updatedNode = (op.getSourceNode().getNodeType()==Node.TEXT_NODE)? op.getSourceNode():op.getTempNode();
		
		if(updatedNode.getNodeType()!=Node.TEXT_NODE) {
			Node sourceNode = op.getSourceNode();

			// Move all the children
			while (updatedNode.hasChildNodes()) {
				sourceNode.appendChild(updatedNode.getFirstChild());
			}
							
			// Replace the updated node by source node
			updatedNode.getParentNode().replaceChild(sourceNode, updatedNode);
			op.setTempNode(null);
		}
	}

	private void undoPreInsert(Operation op) {
		Node insertedNode = op.getSourceNode();		
		insertedNode.getParentNode().removeChild(insertedNode);
		op.setSourceNode(null);
	}

	private void undoPreDelete(Operation op) {
		//Nothing to do
	}

	private void undoPreMove(Operation op) {		
		Node insertedNode = op.getTempNode();
		insertedNode.getParentNode().removeChild(insertedNode);

		op.setTempNode(null);
	}

//	private void undoPreCopy(Operation op) {
//		Node copiedNode = op.getTempNode(); 
//		copiedNode.getParentNode().removeChild(copiedNode);
//		op.setTempNode(null);
//	}
	
	
	/**
	 * invert a checked operation
	 * @param op operation to invert
	 */
	public void undo(Operation op){
		if(op.isExecuted()){
			switch(op.getType()){
			case UPDATE:
				undoUpdate(op);
				break;
			case INSERT:
				undoInsert(op);
				break;
			case MOVE:
				undoMove(op);
				break;
			case DELETE:
				undoDelete(op);
				break;
//			default://COPY
//				undoCopy(op);
			}
			
			op.setExecuted(false);
			op.setPreviewed(false);
			undoList.remove(op);
		}
	}
	

	private void undoUpdate(Operation op) {
		Node updatedNode = op.getSourceNode();
		Node sourceNode = op.getTempNode();
		
		if(sourceNode.getNodeType()==Node.ELEMENT_NODE && updatedNode.getNodeType()==Node.ELEMENT_NODE){			
			// Move all the children
			while (updatedNode.hasChildNodes()) {
				sourceNode.appendChild(updatedNode.getFirstChild());
			}
			
			// Replace the updated node with the first source node
			sourceNode.getParentNode().replaceChild(sourceNode, updatedNode);
			
			// Coz change source node, must update operations which subject (source, target) was source node
			for(int i=1;i<eLog.size();i++){
				Operation op2 = eLog.getOperation(i);
				if(op2.getSourceNode()!=null && op2.getSourceNode().equals((Node)updatedNode)){
					op2.setSourceNode(sourceNode);
				}
				
				if(op2.getTargetNode()!=null && op2.getTargetNode().equals((Node)updatedNode)){
					op2.setTargetNode(sourceNode);
				}
			}
			
			op.setSourceNode(sourceNode);
			op.setTempNode(null);
		}
		else{//TEXT_NODE, just copy the content of node in target tree
			//get old value
//			updatedNode.getParentNode().replaceChild(source.adoptNode(sourceNode), updatedNode);
			updatedNode.setNodeValue(sourceNode.getNodeValue());
		}
	}

	private void undoInsert(Operation op) {
		Node insertedNode = op.getSourceNode();		
		insertedNode.getParentNode().removeChild(insertedNode);
		op.setSourceNode(null);
		
		//once operation is undone, disable operations that depend on operation
		if(op.getDeps(false)!=null){
			List<Operation> dOps = op.getDeps(false);  
			
			for(int i=0;i< dOps.size();i++){
				Operation op2 = (Operation)dOps.get(i);
				op2.setTargetNode(null);
				op2.setNbPrevs(op2.getNbPrevs()+1);
				op2.setEnabled(false);
				if(op2.isExecuted()) undo(op2);
			}
		}
				
	}

	private void undoDelete(Operation op) {
		Node deletedNode = op.getTempNode();
		Node parentNode = op.getTargetNode();
		Node siblingNode = (parentNode.hasChildNodes())? parentNode.getChildNodes().item(op.getPos()):null;
		
		//re-insert deleted subtree
		parentNode.insertBefore(source.adoptNode(deletedNode), siblingNode);
		
		op.setTargetNode(null);
		op.setSourceNode(deletedNode);
		op.setTempNode(null);		
	}

	private void undoMove(Operation op) {
		Node parentNode = op.getTempNode();
		Node siblingNode = (parentNode.hasChildNodes())? parentNode.getChildNodes().item(op.getPos()):null;
		
		//get moved node back to first place
		parentNode.insertBefore(op.getSourceNode(),siblingNode);
		
		//once operation is undone, disable operations that depend on operation
		if(op.getDeps(false)!=null){
			List<Operation> dOps = op.getDeps(false);  
			
			for(int i=0;i< dOps.size();i++){
				Operation op2 = (Operation)dOps.get(i);
				op2.setNbPrevs(op2.getNbPrevs()+1);
				op2.setEnabled(false);
				if(op2.isExecuted()) undo(op2);
			}
		}
		
		op.setTempNode(null);
		op.setPos(op.getMergedNodePath().getNodeIndex());
	}

//	private void undoCopy(Operation op) {
//		Node copiedNode = op.getTempNode();
//		copiedNode.getParentNode().removeChild(copiedNode);	
//	}

	
	/**
	 * apply all checked operations 
	 */
	public void apply(){
		Operation op;
		for(int i=0;i<undoList.size();i++){
			op = undoList.get(i);
			op.setConfirmed(true);
		}
		
		undoList.clear();
	}
	
	/**
	 * invert all checked operations and delete definitively these operations and all dependent operations 
	 */
	public void ignore(){
		List<Operation> removeList = new LinkedList<Operation>();
		Operation op;
		
		for(int i=0;i<undoList.size();i++){
			op = undoList.get(i);
			if(removeList.contains((Operation)op)){
				continue;
			}
			removeList.add(op);
			removeList.addAll(op.getDeps(true));
		}
		
		for(int i=0;i<removeList.size();i++){
			op = removeList.get(i);
			undo(op);
			eLog.remove(op);
		}
		
		undoList.clear();
	}
	
	
	/**
	 * invert all checked operations
	 */
	public void cancel(){
		Operation op;
		for(int i=undoList.size()-1;i>=0;i--){
			op = undoList.get(i);
			undo(op);
		}
		
		undoList.clear();
	}


	public Document getSource() {
		return source;
	}

	public void setSource(Document source) {
		this.source = source;
	}

	public Document getMerged() {
		return merged;
	}

	public void setMerged(Document merged) {
		this.merged = merged;
	}

	public EditLog geteLog() {
		return eLog;
	}

}
